/*
 * File: graphics.c
 * Version: 3.2
 * Last modified on Thu Feb 23 13:39:32 2023 by xuehao
 * -----------------------------------------------------
 * This file implements the graphics.h and extgraph.h interfaces
 * for the Borland/Windows platform.
 */

#include "graphics.h"

#include <SDL2/SDL.h>

#include "extgraph.h"
#include "genlib.h"
#include "strlib.h"

/*
 * Parameters
 * ----------
 * DefaultSize     -- Default point size
 * MaxTitle        -- Maximum window title length
 * MaxFontName     -- Maximum font name length
 * MaxFonts        -- Maximum number of fonts
 * LeftMargin      -- Margin from left of screen to both windows
 * RightMargin     -- Minimum margin to right of windows
 * TopMargin       -- Margin from top of screen to graphics window
 * BottomMargin    -- Minimum margin from bottom of screen to console window
 * WindowSep       -- Separation between graphics and console windows
 * ConsoleHeight   -- Height of the console window (pixels)
 * MinConsoleScale -- Smallest acceptable scale factor for the console
 * PStartSize      -- Starting size for polygon (must be greater than 1)
 * MaxColors       -- Maximum number of color names allowed
 * MinColors       -- Minimum number of colors the device must support
 * DefaultFont     -- Font that serves as the "Default" font
 */

#define LeftMargin    10
#define RightMargin   25
#define TopMargin     35
#define BottomMargin  30

/*
 * Other constants
 * ---------------
 * LargeInt  -- Integer too large for a coordinate value
 * Epsilon   -- Small arithmetic offset to reduce aliasing/banding
 * Pi        -- Mathematical constant pi
 * AnyButton -- Union of all mouse buttons
 */

#define LargeInt 16000
#define Epsilon  0.00000000001
#define Pi       3.1415926535

/*
 * Type: graphicsStateT
 * --------------------
 * This structure holds the variables that make up the graphics state.
 */

typedef struct graphicsStateT {
    double cx, cy;
    struct graphicsStateT* link;
}* graphicsStateT;

/*
 * Global variables
 * ----------------
 * initialized      -- TRUE if initialization has been done
 * pauseOnExit      -- TRUE if library should pause when exiting
 * consoleWindow    -- Window handle for console window
 * gWindow          -- Window handoe for graphics window
 * gRenderer        -- Leverage GPU rendering
 * gdc              -- Graphics DC (screen)
 * osdc             -- Offscreen DC (memory backup)
 * osbits           -- Offscreen bitmap
 * drawPen          -- Pen used for drawing
 * erasePen         -- Pen used for erasing
 * nullPen          -- Pen used for filling
 * drawColor        -- Color used for drawing
 * eraseColor       -- Color used for erasing
 * windowTitle      -- Current window title (initialized statically)
 * xResolution      -- Horizontal resolution of screen (dots per inch)
 * yResolution      -- Vertical resolution of screen (dots per inch)
 * windowWidth      -- Width of graphics window (inches)
 * windowHeight     -- Height of graphics window (inches)
 * pixelWidth       -- Width of graphics window (pixels)
 * pixelHeight      -- Height of graphics window (pixels)
 * fontTable        -- Table of stored fonts
 * nFonts           -- Number of fonts in fontTable
 * currentFont      -- Index of current font in fontTable
 * regionState      -- Current state of the region
 * regionDensity    -- Fill density to apply to region
 * polygonPoints    -- Array of points used in current region
 * nPolygonPoints   -- Number of active points
 * polygonSize      -- Number of allocated points
 * polygonBounds    -- Bounding box of polygon
 * colorTable       -- Table of defined colors
 * nColors          -- Number of defined colors
 * previousColor    -- Last color index set
 * stateStack       -- Stack of graphicStateT blocks
 * cx, cy           -- Current coordinates     | These
 * eraseMode        -- Setting of erase flag   | variables
 * textFont         -- Current font            | consititute
 * textStyle        -- Current style           | the current
 * pointSize        -- Current point size      | graphics
 * penColor         -- Color of pen            | state
 */

static bool initialized = false;

static SDL_Color drawColor;
static SDL_Color eraseColor;
static string windowTitle = "Graphics Window";

static double windowWidth = 640;
static double windowHeight = 480;
SDL_Window* gWindow = NULL;
SDL_Renderer* gRenderer = NULL;

static graphicsStateT stateStack;
static double cx, cy;

/* Private function prototypes */

static void InitCheck(void);
static void InitGraphicsState(void);
static void InitDisplay(void);
static void InitDrawingTools(void);
static void DisplayClear(void);
static double Radians(double degrees);

/* Exported entries */

/* Section 1 -- Basic functions from graphics.h */

void InitGraphics(void) {
    if (!initialized) {
        initialized = true;
        InitDisplay();
    }
    DisplayClear();
    InitGraphicsState();
}

void MovePen(double x, double y) {
    InitCheck();
    cx = x * GetXResolution();
    cy = y * GetYResolution();
}

void DrawLine(double dx, double dy) {
    InitCheck();
    SDL_SetRenderDrawColor(gRenderer, drawColor.r, drawColor.g, drawColor.b, drawColor.a);
    dx *= GetXResolution();
    dy *= GetYResolution();
    SDL_RenderDrawLine(gRenderer, cx, cy, cx + dx, cy + dy);
    cx += dx;
    cy += dy;
}

void DrawArc(double r, double start, double sweep) {
    DrawEllipticalArc(r, r, start, sweep);
}

double GetWindowWidth(void) {
    InitCheck();
    return (windowWidth);
}

double GetWindowHeight(void) {
    InitCheck();
    return (windowHeight);
}

double GetCurrentX(void) {
    InitCheck();
    return (cx);
}

double GetCurrentY(void) {
    InitCheck();
    return (cy);
}

void WaitQuit(void) {
    bool running = true;
    while (running) {
        SDL_Event ev;
        while (SDL_PollEvent(&ev) != 0) {
            if (ev.type == SDL_QUIT) {
                running = false;
                ExitGraphics();
            }
        }
        Pause(100);
    }
}

/* Section 2 -- Elliptical arcs */

void DrawEllipticalArc(double rx, double ry, double start, double sweep) {
    InitCheck();
    SDL_SetRenderDrawColor(gRenderer, drawColor.r, drawColor.g, drawColor.b, drawColor.a);

    rx *= GetXResolution();
    ry *= GetYResolution();
    double center_x = cx - rx;
    double center_y = cy;

    float x, y;
    for (double angle = 0; angle < sweep; angle += 0.1) {
        x = center_x + rx * cos(Radians(start + angle));
        y = center_y + ry * sin(Radians(start + angle + 180));
        SDL_RenderDrawPointF(gRenderer, x, y);
    }

    cx = x;
    cy = y;
}

/* Section 7 -- Miscellaneous functions */

void SetWindowTitle(string title) {
    windowTitle = CopyString(title);
    if (initialized) {
        SDL_SetWindowTitle(gWindow, windowTitle);
    }
}

string GetWindowTitle(void) {
    return (CopyString(windowTitle));
}

void UpdateDisplay(void) {
    InitCheck();
    SDL_RenderPresent(gRenderer);
}

void Pause(double seconds) {
    SDL_Delay(seconds);
}

void ExitGraphics(void) {
    SDL_DestroyRenderer(gRenderer);
    gRenderer = NULL;
    SDL_DestroyWindow(gWindow);
    gWindow = NULL;
    SDL_Quit();
}

void SaveGraphicsState(void) {
    InitCheck();
    graphicsStateT sb = New(graphicsStateT);
    sb->cx = cx;
    sb->cy = cy;
    sb->link = stateStack;
    stateStack = sb;
}

void RestoreGraphicsState(void) {
    InitCheck();
    if (stateStack == NULL) {
        Error("RestoreGraphicsState called before SaveGraphicsState");
    }
    graphicsStateT sb = stateStack;
    cx = sb->cx;
    cy = sb->cy;
    stateStack = sb->link;
    FreeBlock(sb);
}

double GetFullScreenWidth(void) {
    SDL_DisplayMode dm;
    if (SDL_GetDesktopDisplayMode(0, &dm) != 0) {
        SDL_Log("SDL_GetDesktopDisplayMode failed: %s", SDL_GetError());
        return 0.0;
    }
    return (double)dm.w;
}

double GetFullScreenHeight(void) {
    SDL_DisplayMode dm;
    if (SDL_GetDesktopDisplayMode(0, &dm) != 0) {
        SDL_Log("SDL_GetDesktopDisplayMode failed: %s", SDL_GetError());
        return 0.0;
    }
    return (double)dm.h;
}

void SetWindowSize(double width, double height) {
    if (initialized)
        return;
    windowWidth = width;
    windowHeight = height;
}

double GetXResolution(void) {
    int ww;
    int wh;
    SDL_GetWindowSize(gWindow, &ww, &wh);
    int dw;
    int dh;
    SDL_GL_GetDrawableSize(gWindow, &dw, &dh);
    return (double)dw / (double)ww;
}

double GetYResolution(void) {
    int ww;
    int wh;
    SDL_GetWindowSize(gWindow, &ww, &wh);
    int dw;
    int dh;
    SDL_GL_GetDrawableSize(gWindow, &dw, &dh);
    return (double)dh / (double)wh;
}

/* Private functions */

/*
 * Function: InitCheck
 * Usage: InitCheck();
 * -------------------
 * This function merely ensures that the package has been
 * initialized before the client functions are called.
 */

static void InitCheck(void) {
    if (!initialized)
        Error("InitGraphics has not been called");
}

/*
 * Function: InitGraphicsState
 * Usage: InitGraphicsState();
 * ---------------------------
 * This function initializes the graphics state elements to
 * their default values.  Because the erase mode and font
 * information are also maintained in the display state,
 * InitGraphicsState must call functions to ensure that these
 * values are initialized there as well.
 */

static void InitGraphicsState(void) {
    cx = cy = 0;
}

/*
 * Function: InitDisplay
 * Usage: InitDisplay();
 * ---------------------
 * This function does everything necessary to initialize the display.
 * In this implementation, the graphics window is created as an
 * overlapping child of the console window, which has been created
 * by EasyWin.  As a child of the console, the window automatically
 * gets events whenever the console window is waiting for events,
 * simplifying the logic of the implementation considerably.  Most
 * of the InitDisplay implementation is concerned with calculating
 * the new tiling geometry, after which the function creates the
 * new window and an offscreen bitmap to use as a target for any
 * rendering.  When the window gets a Paint event in the event
 * procedure GraphicsEventProc, all it has to do is copy the
 * bits from the offscreen bitmap onto the screen.
 */

static void InitDisplay(void) {
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        SDL_Log("SDL could not initialize! SDL_Error: %s\n", SDL_GetError());
    } else {
        Uint32 window_flags = SDL_WINDOW_ALLOW_HIGHDPI;
        gWindow = SDL_CreateWindow(windowTitle, LeftMargin, TopMargin, windowWidth, windowHeight,
                                   window_flags);
        if (gWindow == NULL) {
            SDL_Log("Window could not be created! SDL_Error: %s\n", SDL_GetError());
        } else {
            InitDrawingTools();
            Uint32 renderer_flags = SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC;
            gRenderer = SDL_CreateRenderer(gWindow, -1, renderer_flags);
            if (SDL_SetRenderDrawColor(gRenderer, eraseColor.r, eraseColor.g, eraseColor.b,
                                       eraseColor.a) < 0) {
                SDL_Log("SDL could not initialize! SDL_Error: %s\n", SDL_GetError());
            }
        }
    }
}

/*
 * Function: InitDrawingTools
 * Usage: InitDrawingTools();
 * --------------------------
 * This function initializes all of the standard objects used for
 * drawing except for fonts, which are initialized dynamically by
 * the DisplayFont procedure.  This function creates the
 * foreground/background colors, the drawing pens, and the brushes
 * for filled regions.
 */

static void InitDrawingTools(void) {
    drawColor = (SDL_Color){0, 0, 0, 255};
    eraseColor = (SDL_Color){255, 255, 255, 255};
}

/*
 * Function: DisplayClear
 * Usage: DisplayClear();
 * ----------------------
 * This function renders the background color.
 */

static void DisplayClear(void) {
    InitCheck();
    SDL_RenderClear(gRenderer);
}

/*
 * Function: Radians
 * Usage: radians = Radians(degrees);
 * ----------------------------------
 * This functions convert an angle in degrees to radians.
 */

static double Radians(double degrees) {
    return (degrees * Pi / 180);
}